package cn.remex.task;

import cn.remex.core.exception.NestedException;
import cn.remex.core.exception.ServiceCode;
import cn.remex.core.quartz.Job;
import cn.remex.core.util.Assert;
import cn.remex.core.util.Judgment;
import cn.remex.core.util.LocalCacheWithLRU;
import cn.remex.core.util.StringHelper;
import cn.remex.db.Database;
import cn.remex.task.model.SysTask;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Disc: 定时任务基础类
 * Created by xinxinran on 2017/3/12.
 */
public abstract class TaskBaseJob extends Job {
    protected static final Logger logger = LoggerFactory.getLogger(TaskBaseJob.class);

    private static final LocalCacheWithLRU<SysTask> jobExecutingCache = new LocalCacheWithLRU<>(1024);
    @Override
    public void jobExecute(JobExecutionContext ctx) throws JobExecutionException {
        // 取出定时任务本身信息实例 参见 TaskController
        SysTask sysTask = (SysTask) ctx.getJobDetail().getJobDataMap().get("SysTask");

        try {
            Assert.notNull(sysTask, ServiceCode.FAIL, "定时任务配置不能为空");

            sysTask = Database.select(SysTask.class).withBase().filterById(sysTask.getId()).execute().obtainBean();
            ctx.getJobDetail().getJobDataMap().put("SysTask", sysTask);

            if(sysTask.getProcessStatus() != TaskConst.ProcessStatus.DOING){
                logger.info("定时任务状态不可运行，忽略本次调用。TaskId : {}, TaskStatus : {}", sysTask.getId(), sysTask.getProcessStatus());
                return;
            }

            if(!enableParallelExecute() && jobExecutingCache.get(sysTask.getId()) != null){
                logger.debug("定时任务已经存在运行线程，忽略本次调用。TaskId : {}, TaskStatus : {}", sysTask.getId(), sysTask.getProcessStatus());
                return;
            }else if(!enableParallelExecute()){
                jobExecutingCache.put(sysTask.getId(), sysTask);
            }

            doJob(sysTask, ctx);

            //只能修改状态以及参数
            Database.update(SysTask.class)
                    .assignColumn(SysTask::getParams, sysTask.getParams())
                    .assignColumn(SysTask::getProcessStatus, sysTask.getProcessStatus())
                    .assignColumn(SysTask::getExpectStartTime, sysTask.getExpectStartTime())
                    .assignColumn(SysTask::getProcessResult, sysTask.getProcessResult())
                    .filterById(sysTask.getId())
                    .execute().assertEffectOneRow(ServiceCode.FAIL, "更新sysTask配置数据错误");

            logger.info("定时任务处理完成：{}", sysTask);
        } catch (NestedException e) {
            //所有跑出信审异常的认为是任务失败而不是错误
            logger.error("定时任务处理失败", e);
            boolean forceResetStatus = false;

            try{forceResetStatus = handleBusinessException(sysTask, ctx, e);}catch (Exception ignored){};
            Database.update(SysTask.class)
                    .assignColumn(SysTask::getParams, sysTask.getParams())
                    .assignColumn(SysTask::getProcessStatus, forceResetStatus ? sysTask.getProcessStatus() : TaskConst.ProcessStatus.FAIL)
                    .assignColumn(SysTask::getExpectStartTime, sysTask.getExpectStartTime())
                    .assignColumn(SysTask::getProcessResult, Judgment.nullOrBlank(e.getMessage()) ? StringHelper.obtainStackMsg(e, 2048) : e.getMessage())
                    .filterById(sysTask.getId())
                    .execute().assertEffectOneRow(ServiceCode.FAIL, "更新sysTask配置数据错误 FAIL 失败");
        } catch (Throwable t) {
            //框架非业务异常和系统未考虑到的异常，则任务服务错误
            logger.error("定时任务理错误", t);
            boolean forceResetStatus = false;
            try{ forceResetStatus = handleUnexpectedError(sysTask, ctx, t);}catch (Exception ignored){};
            Database.update(SysTask.class)
                    .assignColumn(SysTask::getParams, sysTask.getParams())
                    .assignColumn(SysTask::getProcessStatus, forceResetStatus ? sysTask.getProcessStatus() : TaskConst.ProcessStatus.ERROR)
                    .assignColumn(SysTask::getExpectStartTime, sysTask.getExpectStartTime())
                    .assignColumn(SysTask::getProcessResult, StringHelper.obtainStackMsg(t, 2048))
                    .filterById(sysTask.getId())
                    .execute().assertEffectOneRow(ServiceCode.FAIL, "更新sysTask配置数据错误 ERROR 失败");
        }

        //  执行之后必须要删除，否则job将无法再次进入
        if(Judgment.notEmpty(sysTask)){
            jobExecutingCache.remove(sysTask.getId());
        }
    }

    /**
     * 是否允许上次任务未执行完成时，进入本次任务执行，默认是false，如果希望允许，请在子类覆写此方法为true
     *
     * @return false, 不允许，若存在同一个SysTask正在执行job，则忽略本次执行   true，允许，可以多个线程同时执行同一个SysTask
     */
    protected boolean enableParallelExecute(){
        return false;
    }

    /**
     * 定时任务执行具体业务内容
     *
     * @param sysTask 定时任务信息实例，可以在处理业务时修改ProcessStatus状态，以及Params参数。不用返回，直接set即可
     * @param ctx     定时任务上下文环境，可以拿到更多的任务信息以及额外参数
     */
    protected abstract void doJob(SysTask sysTask, JobExecutionContext ctx);

    /**
     * 处理定时任务发生的业务异常
     *
     * @param sysTask   定时任务信息实例，可以在处理业务时修改ProcessStatus状态，以及Params参数。不用返回，直接set即可
     * @param ctx       定时任务上下文环境，可以拿到更多的任务信息以及额外参数
     * @param exception 抛出的业务异常
     * @return 是否重写任务状态，如果为true，则以sysTask实例的状态为最终状态；否则，使用默认状态FAIL
     */
    protected abstract boolean handleBusinessException(SysTask sysTask, JobExecutionContext ctx, NestedException exception);

    /**
     * 处理定时任务发生的非业务错误
     *
     * @param sysTask   定时任务信息实例，可以在处理业务时修改ProcessStatus状态，以及Params参数。不用返回，直接set即可
     * @param ctx       定时任务上下文环境，可以拿到更多的任务信息以及额外参数
     * @param throwable 抛出的非业务错误
     * @return 是否重写任务状态，如果为true，则以sysTask实例的状态为最终状态；否则，使用默认状态ERROR
     */
    protected abstract boolean handleUnexpectedError(SysTask sysTask, JobExecutionContext ctx, Throwable throwable);
}
